#!/usr/bin/env php
<?php

// get additional include file for extension: --include/-i
// get the deployment recipe: recipe
// ex: chief.php recipe.json
// ex: chief.php -i=./custom_includes.php recipe.json

$argv = Chief::parseCliArgs($argv);
//print_r($argv);

if (!isset($argv['input'][1])) {
	Chief::help();
}

$recipeFilePath = $argv['input'][1];
if (!file_exists($recipeFilePath)) {
	echo "File $recipeFilePath not found!!!";
	exit();
}

$recipe = new Recipe($recipeFilePath);
Chief::cook($recipe);

// driver class
class Chief {

	public static function cook($recipe) {
		if (empty($recipe)) {
			echo "no recipe object found!\n";
			exit();
		}

		//print_r($recipe->targets);
		//print_r($recipe->commandsPerTarget);
		foreach ($recipe->commandsPerTarget as $target => $recipes) {
			$remote_host = isset($recipe->targets[$target]['conf']['host']) ? $recipe->targets[$target]['conf']['host'] : null;
			$remote_user = isset($recipe->targets[$target]['conf']['user']) ? $recipe->targets[$target]['conf']['user'] : null;

			echo "[target]: $target\n";
			foreach ($recipes as $command_name => $commands) {
				echo "\t[recipe]: $command_name\n";
				foreach ($commands as $command) {
					list($directive, $command_text) = each($command);
					if ($directive == 'desc') {
						echo "\t[desc]: $command_text\n";
					} else if ($directive == 'run') {
						//echo "-------->[cmd]: $command_text\n";
						self::run($command_text, $lines, $status);
					} else if ($directive == 'run-remote') {
						if (empty($remote_host)) {
							echo "\t\tignoring remote command, since remote host is not defined\n";
							echo "\t\t[ignoring]: $command_text\n";
						} else if (empty($remote_user)) {
							echo "\t\tignoring remote command, since remote user is not defined\n";
							echo "\t\t[ignoring]: $command_text\n";
						} else {
							//echo "-------->[cmd on $remote_host]: $command_text\n";
							self::run_remote($remote_user, $remote_host, $command_text, $lines, $status);
						}
					}
				}
			}
		}
	}

	public static function run($cmd, $lines = null, $status = null) {
		echo "[cmd]: $cmd\n";
		$result = exec($cmd, $lines, $status);
		self::print_cmd_outputs($lines);

		if ($status != 0) {
			echo "!!!command failed with status $status! \n";
			exit($status);
		}
		return $result;
	}

	public static function run_remote($user, $host, $cmd, $lines = null, $status = null) {
		$cmd = str_replace("'", "\'", $cmd);
		$cmd = "ssh $user@$host '".$cmd."'";
		echo "[cmd]: $cmd\n";
		$result = exec($cmd, $lines, $status);
		self::print_cmd_outputs($lines);

		if ($status != 0) {
			echo "!!!command failed with status $status! \n";
			exit($status);
		}

		return $result;
	}

	private static function print_cmd_outputs($lines) {
		foreach ($lines as $line) {
			echo "[output] $line\n";
		}
	}

	public static function parseCliArgs($argv) {
		$_ARG = array();

		function merge_global_options($optionName) {
			$globalOptions = array('h' => 'help', 'v' => 'verbose', 'i' => 'include', 'p' => 'pretend',
								   'l' => 'list', 'b' => 'rollback', 'f' => 'rollforward', 'n' => 'number');
			return isset($globalOptions[$optionName]) ? $globalOptions[$optionName] : $optionName;
		}

		foreach ($argv as $arg) {
			if (ereg('^-+([[a-zA-Z0-9]+)=(.*)$',$arg,$reg)) { // --run=task1,task2,task3, -r=task1,task2,task3
				$_ARG[merge_global_options($reg[1])] = $reg[2];
			} elseif(ereg('^-+([a-zA-Z0-9]+)$',$arg,$reg)) { // -h, --help
				$_ARG[merge_global_options($reg[1])] = true;
			} else {
				$_ARG['input'][]=$arg; // a b c
			}
		}

		return $_ARG;
	}

	public static function help() {
		echo <<<HELPTEXT
Examples:
chief.php recipe.json
chief.php -i=./custom_includes.php recipe.json
chief.php recipe.json -p
chief.php recipe.json -v

HELPTEXT;
	}

	//TODO add rollback, and rollforward functionality
}

// recipe base class
class Recipe { # contains a set of commands, can contain other sub recipes

	public $conf = array(); // global conf
	public $roles = array();
	public $targets = array();
	public $recipies = array();
	public $commandsPerTarget = array();

	public function __construct($recipeFilePath) {
		// load recipe json file
		// process the recipe into commands in different stage labels
		$json = json_decode(file_get_contents($recipeFilePath), true);
		if (empty($json)) {
			echo "invalid recipe: json error\n";
			return null;
		}

		if (!isset($json['roles'])) {
			echo "invalid recipe: no roles defined\n";
			return null;
		}

		if (!isset($json['targets'])) {
			echo "invalid recipe: no targets defined\n";
			return null;
		}

		if (!isset($json['recipes'])) {
			echo "invalid recipe: no recipes defined\n";
			return null;
		}

		$this->conf = isset($json['conf']) ? $json['conf'] : null;
		$this->roles = $json['roles'];
		$this->targets = $json['targets'];
		$this->recipes = $json['recipes'];

		// process global conf by searching for all of the "run" directive in global conf
		$this->conf = $this->_processRunInConf($this->conf);

		$this->mergeConfAndRecipesForTargets();
		$this->substituteConfVariablesInRecipes();

		$this->getCommandsForTargetsInAllRoles();
		//print_r($this->targets); print_r($this->commandsPerTarget);
	}

	private function _processRunInConf($array) {
		foreach ($array as $key => $value) {
			if (is_array($value) ) {
				if (isset($value['run'])) {
					$array[$key] = Chief::run($value['run']);
					continue;
				}
				$array[$key] = $this->_processRunInConf($value);
			}
		}
		return $array;
	}

	private function mergeConfAndRecipesForTargets() {
		foreach ($this->targets as $target_name => $target) {
			$conf = isset($target['conf']) ? $target['conf'] : array();
			$recipes = isset($target['recipes']) ? $target['recipes'] : array();

			$target['conf'] = array_merge($this->conf, $conf);
			$target['recipes'] = array_merge($this->recipes, $recipes);

			$this->targets[$target_name] = $target;
		}
		//print_r($this->targets);
	}

	private function substituteConfVariablesInRecipes() {
		//print_r($this->targets);
		foreach ($this->targets as $target_name => $target) {
			$conf = $target['conf'];
			$recipes = $target['recipes'];
			foreach ($recipes as $recipe_name => $recipe) {
				$recipes[$recipe_name] = $this->substituteConfVariablesForOneRecipe($conf, $recipe);
			}

			$this->targets[$target_name]['recipes'] = $recipes;
		}
		//print_r($this->targets);
	}

	private function substituteConfVariablesForOneRecipe($conf, $recipe) {
		foreach ($recipe as $directive => $content) {
			$names = array_keys($conf);
			foreach($names as $i => $name) { $names[$i] = '${'.$name.'}'; }
			$values = array_values($conf);
			$recipe[$directive] = str_replace($names, $values, $content);
		}
		return $recipe;
	}

	private function getCommandsForTargetsInAllRoles() {//print_r($this->roles); print_r($this->targets);
		foreach ($this->roles as $role_name => $role) {
			$role_targets = $role['targets'];
			$role_recipes = $role['recipes'];
//print_r($role_targets); print_r($role_recipes);

			// merge the recipe names accordingly (adding the before, after automatically)
			foreach ($role_targets as $i => $target) {
				//print_r($target);
				//print_r($role_recipes);
				$merged_recipes = array();
				foreach ($role_recipes as $j => $recipe) {
					$target_recipes = $this->targets[$target]['recipes']; //print_r($target_recipes);
					if (isset($target_recipes['before-'.$recipe])) {
						$merged_recipes['before-'.$recipe] = $target_recipes['before-'.$recipe];
					}
					if (isset($target_recipes[$recipe])) {
						$merged_recipes[$recipe] = $target_recipes[$recipe];
					}
					if (isset($target_recipes['after-'.$recipe])) {
						$merged_recipes['after-'.$recipe] = $target_recipes['after-'.$recipe];
					}
				}
				//print_r($merged_recipes);
				// merge the recipes including other recipes
				$merged_recipes = $this->substituteRecipeDirective($merged_recipes, $target_recipes);

				$this->commandsPerTarget[$target] = $merged_recipes;
			}
			//print_r($this->commandsPerTarget);
		}
	}

	private function substituteRecipeDirective($merged_recipes, $all_recipes) {
		//print_r($merged_recipes); print_r($all_recipes);

		foreach ($merged_recipes as $recipe_name => $recipe) {
			$merged_recipe = array();
			foreach ($recipe as $commands) {
				list($directive, $command) = each($commands);
//echo $directive.':'.$command."\n";
				if ($directive != 'recipe') {
					$merged_recipe[] = $commands;
					continue;
				}

				// recipe case. try to load the recipe from all => merged
				if (!isset($all_recipes[$command])) {
					echo "couldn't find recipe: [$command]\n";
					continue;
				}

				// ok, found! now merge
				$include_recipe = $all_recipes[$command];
				foreach ($include_recipe as $commands) {
					list($directive, $command) = each($commands);
					if ($directive != 'desc') {
						$merged_recipe[] = array($directive => $command);
					}
				}
			}
			$merged_recipes[$recipe_name] = $merged_recipe;
		}
		return $merged_recipes;
	}
}

?>